package inabit.agent;

import inabit.base.RuleBase;
import inabit.base.WorkingMemory;
import inabit.crisp.inference.InferenceFactoryImpl;
import inabit.inference.BackwardEngine;
import inabit.inference.ForwardEngine;
import inabit.inference.InferenceFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**@author Rafael Rocha
 * Manages the sensors and actuators, the mediator
 * does have a rule base and working memory as well as inference engines
 * to help the decision process*/

public abstract class Mediator {
	
	private Map<String, Map<String, Actuator>> actuatorMap;
	private Map<String, Map<String, Sensor>> sensorMap;
	private Map<String, Actuator> noSetActuator;
	private Map<String, Sensor> noSetSensor;
	private WorkingMemory wm;
	private RuleBase rb;
	private ForwardEngine forwardEngine;
	private BackwardEngine backwardEngine;

	public Mediator() {
		actuatorMap = new HashMap<String, Map<String, Actuator>>();
		sensorMap = new HashMap<String, Map<String, Sensor>>();
		noSetActuator = new HashMap<String, Actuator>();
		noSetSensor = new HashMap<String, Sensor>();
		buildInferenceMachine(new InferenceFactoryImpl());
	}

	
	/**Initialize the working memory, rule base and the inference engines
	 * based on the factory passed, by default these are initialized to inference
	 * over crisp data*/
	public void buildInferenceMachine(InferenceFactory inferenceFactory) {
		this.wm = inferenceFactory.createWorkingMemory();
		this.rb = inferenceFactory.createRuleBase();
		this.forwardEngine = inferenceFactory.createForwardInferenceEngine();
		this.backwardEngine = inferenceFactory.createBackwardInferenceEngine();
	}

	protected RuleBase getRuleBase() {
		return rb;
	}

	protected WorkingMemory getWorkingMemory() {
		return wm;
	}

	protected ForwardEngine getForwardEngine() {
		return forwardEngine;
	}

	protected BackwardEngine getBackwardEngine() {
		return backwardEngine;
	}

	/**Attaches a sensor to the mediator so that the sensor state is verified by the
	 * <i>update</i> method of mediator and if the sensor
	 * state has changed the method <i>onStateChanged</i> is called<br><br> if this method
	 * is used the sensor is attached to no set
	 * <br><br>
	 * if a sensor with the same id is attached it overrides the predecessor, sensors with the
	 * same id within a set are not overriden by this method*/
	public void attachSensor(Sensor sensor) {
		this.noSetSensor.put(sensor.getID(), sensor);
	}
	/**Maps a sensor to a specific set identified by the string <i>setName</i>
	 * sensors attached to a set can be retrieved among others sensors in the same set,
	 * instead of being grouped with all other sensors, sensor cannot have the same id
	 * within the same set*/
	public void attachSensorToSet(String setName, Sensor sensor) {
		
		if (!sensorMap.containsKey(setName)) {
			sensorMap.put(setName, new HashMap<String, Sensor>());
		}
		this.sensorMap.get(setName).put(sensor.getID(), sensor);
	}

	/**Detaches the sensor from the mediator so that it doesn't get its state
	 * updated, if the sensor is within a set use the method <i>detachSensorFromSet</i>*/
	public void detachSensor(String sensorID) {
		noSetSensor.remove(sensorID);
	}

	/**Detaches a sensor with the specified id that is within a set with the specified name */
	public void detachSensorFromSet(String setName, String sensorID) {
		if (sensorMap.containsKey(setName)
				&& sensorMap.get(setName).containsKey(sensorID)) {
			sensorMap.get(setName).remove(sensorID);
		}
	}
	
	/**Attaches an actuator to the mediator so that it can be called by the */
	public void attachActuator(Actuator actuator) {
		noSetActuator.put(actuator.getID(), actuator);
	}

	/**Attaches the actuator to the specified set of actuators*/
	public void attachActuatorToSet(String set, Actuator actuator) {
		if (!actuatorMap.containsKey(set)) {
			actuatorMap.put(set, new HashMap<String, Actuator>());
		}
		this.actuatorMap.get(set).put(actuator.getID(), actuator);
	}

	/**Detach the actuator with the passed id if it isn't within any set*/
	public void dettachActuator(String actuatorID) {
		noSetActuator.remove(actuatorID);
	}

	/**Detach the actuator with the specified id it it is within the specified set*/
	public void dettachActuatorFromSet(String set, String actuatorID) {
		if (actuatorMap.containsKey(set)
				&& actuatorMap.get(set).containsKey(actuatorID)) {
			actuatorMap.get(set).remove(actuatorID);
		}
	}

	public Collection<Sensor> getAllSensors() {
		Set<Sensor> sensors = new HashSet<Sensor>();
		for (Map<String, Sensor> map : sensorMap.values()) {
			sensors.addAll(map.values());
		}
		sensors.addAll(noSetSensor.values());
		return sensors;
	}

	public Collection<Actuator> getAllActuators() {
		Set<Actuator> actuators = new HashSet<Actuator>();
		for (Map<String, Actuator> map : actuatorMap.values()) {
			actuators.addAll(map.values());
		}
		actuators.addAll(noSetActuator.values());
		return actuators;
	}

	/**Returns all the sensor within the specified set*/
	public Collection<Sensor> getSensorsFromSet(String set) {
		return sensorMap.get(set).values();
	}
	
	/**Returns the sensor with the specified id*/
	public Sensor getSensor(String sensorID){
		return noSetSensor.get(sensorID);
	}

	/**Returns all actuators contained within the specified set*/
	public Collection<Actuator> getActuators(String set) {
		return actuatorMap.get(set).values();
	}
	/**Returns the actuator with the specified id which is not within any set*/
	public Actuator getActuator(String actuatorID){
		return noSetActuator.get(actuatorID);
	}
	/**Returns the actuator with the specified id within the specified set*/
	public Actuator getActuatorFromSet(String setId, String id) {
		return actuatorMap.get(setId).get(id);
	}

	/**Execute all actuators within a set calling the <i>execute</i> method passing the specified parameters*/
	protected void executeActuators(String set, String... params) {
		for (Actuator a : actuatorMap.get(set).values()) {
			a.execute(params);
		}
	}

	/**Execute a specific actuator from a specific set passing the parameters defined by <i>params</i> argument*/
	protected void executeActuatorFromSet(String set, String id, String... params) {
		actuatorMap.get(set).get(id).execute(params);
	}
	
	/**Execute an actuator which is not attached to any set, passing the defined parameters */
	protected void executeActuator(String id, String... params){
		noSetActuator.get(id).execute(params);
	}

	/**Returns a new collection containing the intersection between the collection of sensor passed
	 * by parameter with the collection of sensor represented by the specified set*/
	protected Collection<Sensor> getSensorsFromSet(String set, Collection<Sensor> sensors){
		Set<Sensor> sensorsFromSet = new HashSet<Sensor>();
		Map<String, Sensor> sensorSet = sensorMap.get(set);
		for(Sensor s : sensors) {
			String id = s.getID();
			if(sensorSet.containsKey(id) && sensorSet.get(id) == s){
				sensorsFromSet.add(s);
			}
		}
		return sensorsFromSet;
	}
	
	/**Returns a new collection containing the intersection between the collection of actuators passed
	 * by parameter with the collection of actuators represented by the specified set*/
	protected Collection<Actuator> getActuatorsFromSet(String set, Collection<Actuator> actuators){
		Set<Actuator> actuatorsFromSet = new HashSet<Actuator>();
		Map<String, Actuator> actuatorsSet = actuatorMap.get(set);
		for(Actuator s : actuators) {
			String id = s.getID();
			if(actuatorsSet.containsKey(id) && actuatorsSet.get(id) == s){
				actuatorsFromSet.add(s);
			}
		}
		return actuatorsFromSet;
	}
	/** Returns the most recent reading state of the sensor */
	protected String getSensorStateFromSet(String set, String id) {
		return sensorMap.get(set).get(id).getCurrentState();
	}
	
	/**Returns the state of the sensor with the specified id if it isn't within any set*/
	protected String getSensorState(String id){
		return noSetSensor.get(id).getCurrentState();
	}

	/** Returns the sensor's reading from the last update */
	protected String getSensorLastState(String set, String id) {
		return sensorMap.get(set).get(id).getLastState();
	}
	
	/**Returns the specified sensor's last state*/
	protected String getSensorLastState(String id){
		return noSetSensor.get(id).getLastState();
	}

	/**executes the forward inference over the working memory and rule base of this mediator.*/
	protected void forwardInference() throws Exception {
		this.forwardEngine.inference(rb, wm);
	}

	/**executes the backward inference over the working memory and rule base of this mediator.*/
	protected void backwardInference(String goal) throws Exception {
		this.backwardEngine.setGoal(goal);
		this.backwardEngine.inference(rb, wm);
	}

	/** Check the sensors and updates the mediator */
	public void update() {
		List<Sensor> changedSensors = new ArrayList<Sensor>();
		for (Sensor s : noSetSensor.values()) {
			if(s.hasStateChanged()) {
				changedSensors.add(s);
			}
		}
		for (Map<String, Sensor> m : sensorMap.values()) {
			for (Sensor sensor : m.values()) {
				sensor.updateState();
				if (sensor.hasStateChanged()) {
					changedSensors.add(sensor);
				}
			}
		}
		updateState();
		if (!changedSensors.isEmpty()) {
			onStateChanged(changedSensors);
		}
	}

	/**
	 * This method is called every time that a sensor input is different from
	 * the last update
	 */
	protected abstract void onStateChanged(List<Sensor> changedSensors);

	/**
	 * This method is called every time the mediator is updated after checking
	 * the sensors
	 */
	protected abstract void updateState();

}
